/*
 *  Axolotl - Alternate persistence for Grails
 *  Copyright (C) 2008  Juanjo García Latre
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.googlecode.axolotl.jdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.codehaus.groovy.grails.commons.DomainClassArtefactHandler;
import org.codehaus.groovy.grails.commons.GrailsApplication;
import org.codehaus.groovy.grails.commons.GrailsDomainClass;
import org.codehaus.groovy.grails.commons.GrailsDomainClassProperty;
import org.codehaus.groovy.grails.plugins.support.aware.GrailsApplicationAware;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.jdbc.core.PreparedStatementCreator;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlTypeValue;
import org.springframework.util.CachingMapDecorator;

import com.googlecode.axolotl.Database;
import com.googlecode.axolotl.grails.GrailsDomainUtils;
import com.googlecode.axolotl.lazy.LazyEntity;
import com.googlecode.axolotl.lazy.LazyEntitySet;
import com.googlecode.axolotl.query.Criterion;
import com.googlecode.axolotl.query.Equals;
import com.googlecode.axolotl.query.Order;
import com.googlecode.axolotl.query.Query;


public class GrailsDomainSqlFactory implements SqlFactory, GrailsApplicationAware {

	private GrailsApplication application;
	
	private Map convertersCache = new CachingMapDecorator() {
		protected Object create(Object clazz) {
			return new GrailsDomainConverter((Class) clazz);
		}		
	};
	
	
	public void setGrailsApplication(GrailsApplication application) {
		this.application = application;
	}
	
	
	public String getTableName(Class clazz) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		return getTableName(domainClass);
	}
	
	
	public String getColumnName(Class clazz, String property) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		return getColumnName( domainClass.getPropertyByName(property) );
	}
	
	
	public String getSelectRow(Class clazz) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		
		StringBuilder sql = new StringBuilder("select ")
			.append( getColumnName(domainClass.getIdentifier()) );

		GrailsDomainClassProperty[] properties = getPersistentProperties(domainClass);
		for (int i = 0 ; i < properties.length ; i++) {
			sql.append(",").append( getColumnName(properties[i]) );
		}

		sql.append(" from ").append( getTableName(domainClass) )
			.append(" where ").append( getColumnName( domainClass.getIdentifier() ) ).append("=?");
		return sql.toString();
	}

	
	public String getSelectCount(Class clazz, Object criteria) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		GrailsDomainSqlCriteriaBuilder criteriaBuilder = 
				new GrailsDomainSqlCriteriaBuilder(application, domainClass, this);
		CharSequence where = buildWhere(criteriaBuilder, criteria);       

		StringBuilder sql = new StringBuilder(512); 
		sql.append("select count(distinct ");
		sql.append( getTableName(domainClass) )
			.append('.').append( getColumnName( domainClass.getIdentifier() ) );
		sql.append(") from ").append( buildFrom(domainClass, criteriaBuilder) );

		if (where.length() > 0) {
			sql.append(" where ").append(where);
		}

		return sql.toString();
	}
	

	public String getSelectIds(Class clazz, Object criteria, Object order) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		GrailsDomainSqlCriteriaBuilder criteriaBuilder = 
				new GrailsDomainSqlCriteriaBuilder(application, domainClass, this);
		CharSequence orderBy = buildOrderBy(domainClass, criteriaBuilder, order);
		CharSequence where = buildWhere(criteriaBuilder, criteria);

		StringBuilder sql = new StringBuilder(512); 
		sql.append("select ");
		if (criteriaBuilder.needsDistinct()) {
			sql.append("distinct ");
		}

		sql.append( getTableName(domainClass) )
			.append('.').append( getColumnName( domainClass.getIdentifier() ) ); 

		if (criteriaBuilder.needsDistinct()) {         
			for (String o: criteriaBuilder.getOrders()) {
				sql.append(", ").append(o);
			}
		}

		sql.append(" from ").append( buildFrom(domainClass, criteriaBuilder) );

		if (where.length() > 0) {
			sql.append(" where ").append(where);
		}
		if (orderBy.length() > 0) {
			sql.append(" order by ").append(orderBy);
		}
		return sql.toString();
	}

	
	public String getInsertRow(Class clazz) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		GrailsDomainClassProperty[] properties = getPersistentProperties(domainClass);
		
		StringBuilder sql = new StringBuilder()
			.append("insert into ").append( getTableName(domainClass) )
			.append(" (");

		for (int i = 0 ; i < properties.length ; i++) {
			sql.append(i==0 ? "" : ",").append( getColumnName(properties[i]) );
		}
		
		sql.append(") values (");
		
		for (int i = 0 ; i < properties.length ; i++) {
			sql.append(i==0 ? "" : ",").append('?');
		}
		
		sql.append(")");
		return sql.toString();
	}
	
	 
	public String getUpdateRow(Class clazz) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		StringBuilder sql = new StringBuilder()
			.append("update ").append( getTableName(domainClass) )
			.append(" set ");
		
		GrailsDomainClassProperty[] properties = getPersistentProperties(domainClass);
		for (int i = 0; i < properties.length; i++) {
			sql.append( i==0 ? "" : "," );
			sql.append( getColumnName(properties[i]) ).append("=?");
		}

		sql.append(" where ").append( getColumnName( domainClass.getIdentifier() ) ).append("=?");
		return sql.toString();
	}
	
	
	public String getDeleteRow(Class clazz) {
		GrailsDomainClass domainClass = getDomainClass(clazz);
		StringBuilder sql = new StringBuilder()
			.append("delete from ").append( getTableName(domainClass) )
			.append(" where ").append( getColumnName( domainClass.getIdentifier() ) ).append("=?");
		return sql.toString();
	}


	public Converter getConverter(Class clazz) {
		return (Converter) convertersCache.get(clazz);
	}

	//-------------------------------------------------------------------------------------------
	
	protected class GrailsDomainConverter implements Converter {
		
		private final GrailsDomainClass domainClass;
		private final String identifierColumn;
		private final PreparedStatementCreatorFactory preparedInsertCreatorFactory;
		private final PreparedStatementCreatorFactory preparedUpdateCreatorFactory;
		private final PreparedStatementCreatorFactory preparedDeleteCreatorFactory;
		private Class[] subclasses;

		
		public GrailsDomainConverter(Class clazz) {
			this.domainClass = getDomainClass(clazz);
			this.identifierColumn = getColumnName( domainClass.getIdentifier() );
			
			this.preparedInsertCreatorFactory =
					new PreparedStatementCreatorFactory( getInsertRow(clazz), getPersistentTypes(domainClass) );
			this.preparedUpdateCreatorFactory =
					new PreparedStatementCreatorFactory( getUpdateRow(clazz), getPersistentTypesWithId(domainClass) );
			this.preparedDeleteCreatorFactory = 
					new PreparedStatementCreatorFactory( getDeleteRow(clazz), new int[] {getPersistentType(domainClass.getIdentifier())} );
		}
		
		
		public Class[] getSubClasses() {
			if (subclasses == null) {
				if (domainClass.hasSubClasses()) {
					List subDomainClasses = new ArrayList( domainClass.getSubClasses() );
					subclasses = new Class[ subDomainClasses.size() ];
					for (int i = 0; i < subDomainClasses.size(); i++) {
						subclasses[ i ] = ((GrailsDomainClass) subDomainClasses.get(i)).getClazz();
					}
				}
				else {
					subclasses = new Class[0];
				}
			}
			return subclasses;
		}
		

		public RowMapper getEntityReader(final Database database) {
			return new RowMapper() {
				public Object mapRow(ResultSet rs, int row) throws SQLException {
					BeanWrapper instance = new BeanWrapperImpl( domainClass.getClazz() );
					final Object id = rs.getObject(identifierColumn);
					instance.setPropertyValue(domainClass.getIdentifier().getName(), id);
					for (GrailsDomainClassProperty property: domainClass.getPersistentProperties()) {
						Object value = null;
						final Class refType = property.getReferencedPropertyType();
						if (!property.isOneToMany() && !property.isManyToMany()) {
							String columnName = getColumnName(property);
							value = rs.getObject(columnName);
							if (property.isManyToOne() || property.isOneToOne()) {
								if (property.getFetchMode() == GrailsDomainClassProperty.FETCH_LAZY) {
									value = LazyEntity.create(refType, value, database);
								}
								else if (property.getFetchMode() == GrailsDomainClassProperty.FETCH_EAGER) {
									value = database.load(refType, value);
								}
							}
						}
						else if (property.isOneToMany()) {
							String keyName = property.getOtherSide().getName();
							Object keyValue = instance.getWrappedInstance();
							Query query = new Query(refType, new Equals<Object>(keyName, keyValue), null);
							value = new LazyEntitySet(query, database);
						}
						instance.setPropertyValue(property.getName(), value);
					}
					return instance.getWrappedInstance();
				}				
			};
		}


		public RowMapper getIdReader() {
			return new RowMapper() {
				public Object mapRow(ResultSet rs, int row) throws SQLException {
					return rs.getObject(1); //identifierColumn); TODO?
				}				
			};
		}
		
		
		public PreparedStatementCreator getInsertStatementCreator(Object entity) {
			List values = GrailsDomainUtils.getPersistentValues(domainClass, entity);
			return preparedInsertCreatorFactory.newPreparedStatementCreator(values);
		}
		
		
		public PreparedStatementCreator getUpdateStatementCreator(Object entity) {
			List values = GrailsDomainUtils.getPersistentValues(domainClass, entity);
			values.add( GrailsDomainUtils.getIdentifierValue(domainClass, entity) );
			return preparedUpdateCreatorFactory.newPreparedStatementCreator(values);
		}
		
		
		public PreparedStatementCreator getDeleteStatementCreator(Object entity) {
			Object id = GrailsDomainUtils.getIdentifierValue(domainClass, entity);
			return preparedDeleteCreatorFactory.newPreparedStatementCreator( new Object[] {id} );
		}

		
//		private List getPersistentValues(Object entity) {
//			BeanWrapper wrapper = (entity instanceof BeanWrapper) ? (BeanWrapper) entity : new BeanWrapperImpl(entity);
//			List<Object> values = new ArrayList<Object>();			
//			for (GrailsDomainClassProperty property : getPersistentProperties(domainClass)) {
//				Object value = wrapper.getPropertyValue( property.getName() );
//				if ((property.isManyToOne() || property.isOneToOne()) && value != null) {
//					if (LazyEntity.isLazy(value)) {
//						value = LazyEntity.getId(value);
//					}
//					else {
//						value = GrailsDomainUtils.getIdentifierValue(
//									property.getReferencedDomainClass(), value);
//					}
//				}
//				values.add(value);
//			}
//			return values;
//		}	
	}
	
	
	protected String getTableName(GrailsDomainClass domainClass) {
		return SqlNamingUtils.getTableName(domainClass);
//		return underscorize( domainClass.getName() );
	}
	
	
	protected String[] getColumnNames(GrailsDomainClassProperty[] properties) {
		String[] columnNames = new String[ properties.length ]; 
		for (int i = 0; i < columnNames.length; i++) {
			columnNames[i] = getColumnName( properties[i] );
		}
		return columnNames;
	}

	
	protected String getColumnName(GrailsDomainClassProperty property) {
		return SqlNamingUtils.getColumnName(property);
//		StringBuilder name = new StringBuilder( underscorize(property.getName()) );
//		if (property.isManyToOne() || property.isOneToOne()) {
//			name.append("_id");
//		}
//		return name.toString();
	}	
	
	@Deprecated
	protected GrailsDomainClass getDomainClass(Class clazz) {
		return (GrailsDomainClass) application.getArtefact(DomainClassArtefactHandler.TYPE, clazz.getName());
	}
	
	
	@Deprecated
	protected GrailsDomainClassProperty[] getPersistentProperties(GrailsDomainClass domainClass) {
		return GrailsDomainUtils.getPersistentProperties(domainClass);
//		List<GrailsDomainClassProperty> properties = new ArrayList<GrailsDomainClassProperty>();
//		for (GrailsDomainClassProperty property: domainClass.getPersistentProperties()) {
//			if (!property.isOneToMany() && !property.isManyToMany()) {
//				properties.add(property);
//			}
//		}
//		return properties.toArray(new GrailsDomainClassProperty[properties.size()]);
	}
	
	
	protected int[] getPersistentTypes(GrailsDomainClass domainClass) {
		GrailsDomainClassProperty[] properties = getPersistentProperties(domainClass);
		int[] types = new int[ properties.length ];
		for (int i = 0; i < properties.length; i++) {
			types[ i ] = getPersistentType( properties[ i ] );
		}
		return types;
	}
	
	
	protected int[] getPersistentTypesWithId(GrailsDomainClass domainClass) {
		int[] persistentTypes = getPersistentTypes(domainClass);
		int[] typesWithId = new int[ persistentTypes.length + 1 ];
		System.arraycopy(persistentTypes, 0, typesWithId, 0, persistentTypes.length);
		typesWithId[ typesWithId.length - 1 ] = getPersistentType( domainClass.getIdentifier() );
		return typesWithId;
	}
	
	
	protected int getPersistentType(GrailsDomainClassProperty property) {
		Class type = property.getType();
		if (Number.class.isAssignableFrom(type)) {
			return Types.NUMERIC;
		}
		else if (CharSequence.class.isAssignableFrom(type)) {
			return Types.VARCHAR;
		}
		else if (Date.class.isAssignableFrom(type)) {
			return Types.TIMESTAMP;
		}
		else {
			return SqlTypeValue.TYPE_UNKNOWN;
		}		
	}
	
	
	private CharSequence buildFrom(GrailsDomainClass domainClass, GrailsDomainSqlCriteriaBuilder criteriaBuilder) {
		StringBuilder from = new StringBuilder( getTableName(domainClass) ); 

		// append join tables found by criteria builder.
		for (GrailsDomainSqlCriteriaBuilder.JoinInfo join: criteriaBuilder.getJoins()) {
			from.append(", ").append( join.getTable() );
		}
		return from;
	}
	
	
	private CharSequence buildWhere(GrailsDomainSqlCriteriaBuilder criteriaBuilder, Object criteria) {
		String filter = "";

		if (criteria instanceof Criterion) {
			filter = ((Criterion) criteria).accept(criteriaBuilder);
		}
		else if (criteria != null) {
			filter = criteria.toString();
		}

		StringBuilder where = new StringBuilder();

		// append join criteria found by renderer.
		for (GrailsDomainSqlCriteriaBuilder.JoinInfo join: criteriaBuilder.getJoins()) {
			if (where.length() > 0) {
				where.append(" and ");
			}
			where.append( join.getCriterion() );
		}

		// if there was a previous filter, then it should be appended after join criteria.
		if (filter.length() > 0) {
			if (where.length() > 0) {
				where.append(" and ");
			}
			where.append(filter);
		}

		return where;
	}


	private CharSequence buildOrderBy(GrailsDomainClass domainClass, 
			GrailsDomainSqlCriteriaBuilder criteriaBuilder, Object order) {
		
		StringBuilder orderBy = new StringBuilder();
		final Object o = order;//(order == null ? domainClass.getDefaultOrder() : order);

		if (o instanceof Order)	{
			orderBy.append( ((Order) o).accept(criteriaBuilder) );
		}
		else if (o != null)	{
			orderBy.append(o);
		}
		return orderBy;
	}
	
}
